查看原文
其他

PyTorch圣殿 | 传奇NLP攻城狮成长之路(二)

2017-09-15 集智小仙女 集智AI学园

本文为《PyTorch圣殿 | 传奇NLP攻城狮成长之路》系列(二),上篇:传奇NLP攻城狮成长之路(一)

本系列教程由集智小仙女所写,以每周一篇的频率给大家推送。共10期的教程,每周免费连载,重要的事情说三遍:免费!免费!免费!

周周更新,为读者负责到底,让你能把真正的技术学习到手!

本系列全套教程都将在集智AI学园公众号上发布(首发),如需转载,请联系本公众号获得授权。


小试牛刀:编写一个词袋分类器

这位战士你好!在学习了第一课中的PyTorch理论知识后,你已经是一名PyTorch圣殿的NLP见习攻城狮了。在未来,你是否能像前辈们那样用体内的NLP之力点燃那炙热的PyTorch火焰?

在上节课学习了大量理论知识后,你是不是已经按耐不住心中的NLP之力,想搭建一个神经网络试试?

那么你的机会来了!

本期目录:

  • 1.使用PyTorch搭建一个完整的神经网络吧!

  • 2.Logistic回归词袋分类器

  • 3.建立单词表

  • 4.搭建神经网络!

  • 5.未训练的神经网络怎么样?

  • 6.训练、运行神经网络

【本期福利】

集智AI学园公众号回复“传奇NLP攻城狮02”,可以获取本节教程的Jupyter Notebook文档!


1、使用PyTorch搭建一个完整的神经网络吧!

在编写真正能解决自然语言问题的项目之前,让我们先用上节课讲到的“仿射映射”和“非线性函数”搭建一个神经网络。在这个过程中我们将学习到如何使用PyTorch内置的负对数似然估计进行损失函数的计算,以及如何使用反向传播算法更新参数等等。

用于搭建神经网络的组件都可以从torch.nn.Module中继承,在构建神经网络时,我们需要重写它们的forward()方法。我们可以决定这些网络组件是在CPU或者GPU中计算,只要使用组件的.cuda()方法或.cpu()方法就可以灵活的切换。

那么下面就让我们动手搭建这个网络吧!我们这次要搭建的神经网络是一个“稀疏词袋模型(BOW,bag-of-words)”。它有什么功能嘞?我们给它一个单词,它能够预测这个单词是属于英文的,还是属于西班牙文的。这种形式的模型是典型的“二分类Logistic回归模型”。

2、 Logistic回归词袋分类器

这个词袋模型是怎么回事嘞?它其实就是把我们的每个句子映射为一个词袋向量。每个向量都是等长的,假设我们有包括n个单词的单词表,那么我们每个词袋向量的长度就是n。

举个例子来说,假设我们的整个单词表只有两个词“hello”和“world”,那么语句“hello hello hello hello”的词袋向量就是:[4, 0]。

语句“hello world hello world”的词袋向量是[2,2]。

总结来说,对于我们这个单词表只有“hello”,“world”这两个单词的词袋模型,所有语句的词袋向量可以表示为:[Count(hello),Count(world)]

以此类推,更大的单词表,更长的词袋向量也是用同样的方法来计算。

对于我们马上要搭建的神经网络来说,把将要输入的词袋变量设为x,那么神经网络的输出为:

logSoftmax(Ax+b)

其代表的意义就是:输入的数据会首先经过仿射映射运算,然后再进行log Softmax运算,最后输出。

那现在让我们上代码吧!

像上次一样,首先引入环境需要的包。

  1. import torch

  2. import torch.autograd as autograd

  3. import torch.nn as nn

  4. import torch.nn.functional as F

  5. import torch.optim as optim

  6. torch.manual_seed(1)

  1. <torch._C.Generator at 0x108d1d810>

3、建立单词表

下面要进行“数据预处理”。

真正的“数据预处理”是指:在使用数据训练神经网络模型之前,为了让数据更适用于模型训练而对数据采取的操作。一般会采取的操作是对数据进行“去杂(去除不完整的数据)”或者“补充(用某个值补充不完整的数据)”,以及“归一化”,“独热编码”,“数据变形”等等……

我们这次并不对数据进行过多的操作,仅是建立一个单词表而已。

  1. # 虽然数据少了点

  2. # 但这个data就是我们的训练数据集……

  3. data = [ ("me gusta comer en la cafeteria".split(), "SPANISH"),

  4.         ("Give it to me".split(), "ENGLISH"),

  5.         ("No creo que sea una buena idea".split(), "SPANISH"),

  6.         ("No it is not a good idea to get lost at sea".split(), "ENGLISH") ]

  7. # 测试集

  8. test_data = [ ("Yo creo que si".split(), "SPANISH"),

  9.              ("it is lost on me".split(), "ENGLISH")]

  10. # word_to_ix 就是单词表

  11. # 它将每个单词映射为一个独一无二的整数

  12. # 这个整数就是这个单词在词袋向量中的索引(位置)

  13. word_to_ix = {}

  14. # 遍历数据集

  15. # 观察数据的形式

  16. # sent是语句

  17. # _是标签,我们现在用不到所以先忽略

  18. for sent, _ in data + test_data:

  19.    for word in sent:

  20.        # 如果单词表中还没有这个单词

  21.        if word not in word_to_ix:

  22.            # 在单词表的末端添加这个单词

  23.            word_to_ix[word] = len(word_to_ix)

  24. print(word_to_ix)

  25. # 最终单词表的大小,26

  26. VOCAB_SIZE = len(word_to_ix)

  27. # 标签表大小,“SPANISH”,“ENGLISH”:2

  28. NUM_LABELS = 2

  1. {'comer': 2, 'la': 4, 'get': 20, 'idea': 15, 'buena': 14, 'una': 13, 'at': 22, 'sea': 12, 'it': 7, 'creo': 10, 'good': 19, 'me': 0, 'gusta': 1, 'on': 25, 'is': 16, 'not': 17, 'si': 24, 'en': 3, 'No': 9, 'a': 18, 'lost': 21, 'que': 11, 'cafeteria': 5, 'Yo': 23, 'Give': 6, 'to': 8}

  1. import operator

  2. # 将单词表排序打印出来看的更清楚

  3. print(sorted(word_to_ix.items(), key=operator.itemgetter(1), reverse=False))

  1. [('me', 0), ('gusta', 1), ('comer', 2), ('en', 3), ('la', 4), ('cafeteria', 5), ('Give', 6), ('it', 7), ('to', 8), ('No', 9), ('creo', 10), ('que', 11), ('sea', 12), ('una', 13), ('buena', 14), ('idea', 15), ('is', 16), ('not', 17), ('a', 18), ('good', 19), ('get', 20), ('lost', 21), ('at', 22), ('Yo', 23), ('si', 24), ('on', 25)]

4、 搭建神经网络!

建立好单词表后,就可以搭建我们的神经网络模型啦!

因为是“第一次”嘛(^__^),让我们先搭建一个简单点的神经网络,它看上去应该是这样子的:

  1. # 上面说过搭建神经网络的组件都可以在nn.Module中找到

  2. # BoWClassifier就是继承自nn.Module

  3. class BoWClassifier(nn.Module):

  4.    def __init__(self, num_labels, vocab_size):

  5.        # 在这里调用下nn.Module的初始化函数

  6.        # 这是习惯性操作~

  7.        super(BoWClassifier, self).__init__()

  8.        # PyTorch定义了nn.Linear()方法,这个方法其实就是仿射映射

  9.        # 你一定要明白为什么输入维度的大小是vocab_size

  10.        # 为什么输出维度是num_labels

  11.        self.linear = nn.Linear(vocab_size, num_labels)

  12.    def forward(self, bow_vec):

  13.        # 正向传播

  14.        # 输入向量进入线性层进行运算,再进行log softmax得到输出。

  15.        return F.log_softmax(self.linear(bow_vec))

下面再定义两个辅助函数。

makebowvector是用来将语句转化为词袋向量的,第一个参数为语句,第二个参数为单词表。

make_target是将标签转化为整数,即0,1……

  1. # 将语句转化为词向量

  2. def make_bow_vector(sentence, word_to_ix):

  3.    # 创建一个与单词表长度相同,内部元素全为0的词向量

  4.    vec = torch.zeros(len(word_to_ix))

  5.    # 遍历语句中的每个单词,

  6.    # 词向量对应位置就是对应单词出现的次数

  7.    for word in sentence:

  8.        vec[word_to_ix[word]] += 1

  9.    # 将返回值转化为(1*26)的向量

  10.    return vec.view(1, -1)

  11. def make_target(label, label_to_ix):

  12.    # 将本例中的"SPANISH"和“ENGLISH”

  13.    # 转化为“0”和“1”

  14.    return torch.LongTensor([label_to_ix[label]])

实例化我们的模型!

  1. # NUM_LABELS:标签个数

  2. # VOCAB_SIZE:单词表长度

  3. model = BoWClassifier(NUM_LABELS, VOCAB_SIZE)

  4. # 每当我们像上面那样实例化一个模型类的时候,

  5. # 就会执行__init__函数中的

  6. # self.linear = nn.Linear(...)这一语句,

  7. # 这时候神经网络的参数就会被模型保存下来

  8. # 让我们将模型保存的参数打印出来

  9. # 下面首先输出的是仿射映射中的A,接着输出的是b。

  10. for param in model.parameters():

  11.    print(param)

  1. Parameter containing:

  2. Columns 0 to 9

  3. -0.0325  0.1950  0.0864  0.1697 -0.1961 -0.1459 -0.0775  0.1957 -0.1386 -0.1035

  4. 0.1483 -0.1061 -0.1854  0.0135  0.0669  0.1624 -0.0324 -0.0168  0.0230 -0.0272

  5. Columns 10 to 19

  6. -0.1599 -0.0406 -0.1231 -0.0440 -0.0606  0.0666 -0.0405  0.1708  0.0152  0.1358

  7. -0.1411  0.1722 -0.1184  0.1092  0.1180  0.0847  0.1837  0.1188 -0.0732 -0.1597

  8. Columns 20 to 25

  9. -0.0317 -0.0732  0.0726  0.0096 -0.1159 -0.0222

  10. 0.0754  0.0071  0.1476  0.1432  0.1548  0.1291

  11. [torch.FloatTensor of size 2x26]

  12. Parameter containing:

  13. -0.1628

  14. 0.1293

  15. [torch.FloatTensor of size 2]

我们这个神经网路的权重(weight)是个2*26的矩阵,一般简写为W,其实就是仿射映射中Ax+b中的A。

那权重参数的数量是怎么确定的?

让我们再看一下本节开始的神经网络结构图,你就明白了

5、未训练的神经网络怎么样?

如果要运行模型,只要将词袋向量转为autograd.Variable类型,然后传入到模型中即可。

  1. # 取出data中的第一条数据

  2. sample = data[0]

  3. # sample[0] 即语句

  4. # sample[1] 即标签(SPANISH,ENGLISH)

  5. # 将语句转化为词袋向量

  6. bow_vector = make_bow_vector(sample[0], word_to_ix)

  7. # 将词袋向量转化为autograd.Variable类型,传入模型

  8. log_probs = model(autograd.Variable(bow_vector))

  9. # 打印出模型预测出的概率

  10. print(log_probs)

  1. Variable containing:

  2. -0.8630 -0.5480

  3. [torch.FloatTensor of size 1x2]

上面打印出的两个概率值,哪个对应的是“SPANISH”,哪个对应的是“ENGLISH”?我们还没有告诉神经网络。如果我们想训练这个神经网络,就一定要想办法告诉它!所以我们把“SPANISH”设为0,“ENGLISH”设为1,这样模型就知道输出概率中第0个位置代表单词属于“SPANISH”,第1个位置代表单词属于“ENGLISH”的概率。

  1. label_to_ix = { "SPANISH": 0, "ENGLISH": 1 }

在正式开始训练之前,先让我们拿还没训练的模型跑一次,看看没经训练的模型能预测出个什么玩意。

  1. # 我们在训练模型前先直接拿测试数据跑一次

  2. # 以运行出结果,和训练后做对比

  3. # 循环使用测试数据集中的“语句”和“标签”

  4. for instance, label in test_data:

  5.    # 将语句转化为词袋向量,再转化为Variable

  6.    bow_vec = autograd.Variable(make_bow_vector(instance, word_to_ix))

  7.    # 输出对数概率

  8.    log_probs = model(bow_vec)

  9.    # 打印出神经网络对测试集中每句话从属的预测概率

  10.    print(log_probs)

  1. Variable containing:

  2. -1.2611 -0.3332

  3. [torch.FloatTensor of size 1x2]

  4. Variable containing:

  5. -1.1140 -0.3978

  6. [torch.FloatTensor of size 1x2]

上面的程序是让神经网络预测“某句话”是属于“SPANISH”还是“ENGLISH”,下面我们再让神经网络预测单词表中的“一个单词”是属于哪种语言。

  1. # 让神经网络预测单词“creo”是属于哪种语言

  2. # next(model.parameters())是遍历model的权重参数

  3. # [:,word_to_ix["creo"]]找到属于“creo”的那一列权重

  4. print(next(model.parameters())[:,word_to_ix["creo"]])

  1. Variable containing:

  2. -0.1599

  3. -0.1411

  4. [torch.FloatTensor of size 2]

上面打印出的就是“creo”这个神经元与“SPANISH”及“ENGLISH”两个神经元连接的权重参数。

与“SPANISH”连接的权重值高就代表神经网络认为这个单词更属于“SPANISH”,反之亦然。

我们可以观察到,未经训练的神经网络做出的预测没一点准头,和随机差不多。

6、 训练、运行神经网络

那么就让我们训练它吧!我们输入训练数据以让神经网络输出概率,然后再计算损失函数,计算损失函数的梯度,然后反向传播更新参数。损失函数由torch在nn包中提供。我们这次采用负对数似然损失,也就是:nn.NLLLoss()。优化函数我们使用普通的“随机梯度下降算法”,即SGD。

NLLLoss的输入是预测的概率向量和目标标签(0 or 1)。NLLLoss()与交叉熵损失函数nn.CrossEntropyLoss()的区别就是NLLLoss()在输出前进行了logSoftmax()运算。

  1. # 定义我们的负对数似然估计损失函数

  2. loss_function = nn.NLLLoss()

  3. # 定义优化器,本次我们仅使用基本的SGD即可

  4. optimizer = optim.SGD(model.parameters(), lr=

  5. 0.1)

  6. # epoch代表将测试数据集循环多少次

  7. # 要训练出一个可靠的神经网络,epoch通常需要循环多次

  8. # 根据网络结构以及任务的不同,epoch可能为10~50之间,但是也有100~200的情况

  9. # 在某些实验网络中能达到1000~2000

  10. # epoch过多有时候会导致过拟合

  11. # 所以epoch作为超参数之一,是个很有学问的设计问题

  12. for epoch in range(100):

  13.    for instance, label in data:

  14.        # 第一步,因为PyTorch会自动累加梯度

  15.        # 这在我们现在的模型是不需要的

  16.        # 所以我们在每次训练前先清掉上一次的梯度

  17.        model.zero_grad()

  18.        # 第二步,将语句转化为词袋向量

  19.        # 同时我们也要将目标(标签)转化为整数

  20.        # 比如,如果目标语言是“SPANISH”,那么我们将其转化为整数“0”

  21.        # 这样损失函数就知道了,最终计算出的对数概率组中的第0个元素

  22.        # 就是“SPANISH”的对数概率

  23.        bow_vec = autograd.Variable(make_bow_vector(instance, word_to_ix))

  24.        target = autograd.Variable(make_target(label, label_to_ix))

  25.        # 第三步,运行正向传播

  26.        log_probs = model(bow_vec)

  27.        # 第四步,计算损失,梯度,

  28.        loss = loss_function(log_probs, target)

  29.        # 计算损失后,反向传播

  30.        loss.backward()

  31.        # 使用optimizer.step()更新参数

  32.        optimizer.step()

使用训练数据集训练模型后,下面我们可以使用测试集来测试模型的效果了。

  1. for instance, label in test_data:

  2.    bow_vec = autograd.Variable(make_bow_vector(instance, word_to_ix))

  3.    log_probs = model(bow_vec)

  4.    print(log_probs)

  1. Variable containing:

  2. -0.1730 -1.8395

  3. [torch.FloatTensor of size 1x2]

  4. Variable containing:

  5. -3.4680 -0.0317

  6. [torch.FloatTensor of size 1x2]

我们得到了正确的答案!可以看到神经网络对于第一条测试数据得出的预测概率中,“SPANISH”的概率明显更高!对于第二条测试数据,代表“ENGLISH”的概率明显更高!

  1. # 再将模型对“creo”这个西班牙语单词的预测结果打印出来

  2. print(next(model.parameters())[:,word_to_ix["creo"]])

  1. Variable containing:

  2. 0.3932

  3. -0.6942

  4. [torch.FloatTensor of size 2]

从输出中我们可以明显发现,对于“creo”这个词,相比于训练前的模型,模型输出其属于“SPANISH”的概率明显升高,其属于“ENGLISH”的概率明显降低。训练后的模型对词汇从属判断的更加准确了。

在本节课程中,我们体验了搭建一个简单神经网络的整个流程。同时也亲自见识到了神经网络的威力,要知道我们只用了仅仅4条数据训练的神经网络,就已经可以进行正确的预测了。


下一节课我们将接触到NLP中一种非常重要的网络,循环神经网络(RNN),我们会使用它来完成一个“名字归属分类器”!

PyTorch圣殿 | 传奇NLP攻城狮成长之路 课程表

  • 上期:PyTorch概要简析

  • 本期:小试牛刀:编写一个词袋分类器

  • 下期:使用RNN做一个名字分类器

  • 第四期:起名大师:使用RNN生成个好名字

  • 第五期:AI编剧:使用RNN创作莎士比亚剧本

  • 第六期:AI翻译官:采用注意力机制的翻译系统

  • 第七期:探索词向量世界

  • 第八期:词向量高级:单词语义编码器

  • 第九期:长短记忆神经网络(LSTM)序列建模

  • 第十期:体验PyTorch动态编程,双向LSTM+CRF

大家下周再见!


福利获取方式:

集智AI学园公众号回复:传奇NLP攻城狮02



如果您有任何关于Pytorch方面的问题,欢迎进【集智—清华】火炬群与大家交流探讨,添加集智小助手微信swarmaAI,备注(pytorch交流群),小助手会拉你入群。

推荐阅读

AI公司职位内推 | 自动驾驶、智慧医疗、智能家居

传奇NLP攻城狮成长之路(一)

教程 | Windows用户指南:如何用Floyd跑PyTorch

《深入浅出GAN-原理与应用》学习笔记

课程推荐 | 进击的强化学习

AI江湖|筑基篇——人工神经网络

深度 | 人工智能让我们失业?不,这取决于我们自己

学员原创 | 人工智能产品经理的新起点(200页PPT下载)

基于神经机器翻译技术的英文修改器养成记 | 集智原创发明

吐血推荐:超级好用的深度学习云平台Floyd | 集智AI学园


关注集智AI学园公众号

获取更多更有趣的AI教程吧!

搜索微信公众号:swarmAI

集智AI学园QQ群:426390994

学园网站:campus.swarma.org


 商务合作|zhangqian@swarma.org     

投稿转载|wangjiannan@swarma.org

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存